5. Poor Man’s Tab Bar
Apple provides the
UITabBar widget as part of the UIKit framework. This
is the familiar list of gray icons that many applications have along the
bottom of the screen, as shown in Figure 4.
Since UIKit widgets are outside the scope of
this book, you’ll be using OpenGL to create a poor man’s tab bar for
switching between the various parametric surfaces, as in Figure 5.
In many situations like this, a standard
UITabBar is preferable since it creates a more
consistent look with other iPhone applications. But in our case, we’ll
create a fun transition effect: pushing a button will cause it to “slide
out” of the tab bar and into the main viewport. For this level of
control over rendering, UIKit doesn’t suffice.
The wireframe viewer has a total of six
parametric surfaces, but the button bar has only five. When the user
touches a button, we’ll swap its contents with the surface being
displayed in the main viewport. This allows the application to support
six surfaces with only five buttons.
The state for the five buttons and the
button-detection code lives in the application engine. New lines in the
class declaration from ApplicationEngine.cpp are
shown in bold in Example 7. No
modifications to the two rendering engines are required.
Example 7. ApplicationEngine declaration with tab bar
#include "Interfaces.hpp" #include "ParametricEquations.hpp" #include <algorithm>
using namespace std;
static const int SurfaceCount = 6; static const int ButtonCount = SurfaceCount - 1;
class ApplicationEngine : public IApplicationEngine { public: ApplicationEngine(IRenderingEngine* renderingEngine); ~ApplicationEngine(); void Initialize(int width, int height); void OnFingerUp(ivec2 location); void OnFingerDown(ivec2 location); void OnFingerMove(ivec2 oldLocation, ivec2 newLocation); void Render() const; void UpdateAnimation(float dt); private: void PopulateVisuals(Visual* visuals) const; int MapToButton(ivec2 touchpoint) const; vec3 MapToSphere(ivec2 touchpoint) const; float m_trackballRadius; ivec2 m_screenSize; ivec2 m_centerPoint; ivec2 m_fingerStart; bool m_spinning; Quaternion m_orientation; Quaternion m_previousOrientation; IRenderingEngine* m_renderingEngine; int m_currentSurface; ivec2 m_buttonSize; int m_pressedButton; int m_buttonSurfaces[ButtonCount]; };
|
Example 8
shows the implementation. Methods left unchanged (such as MapToSphere) are omitted for brevity.
You’ll be replacing the following methods: ApplicationEngine::ApplicationEngine,
Initialize, Render,
OnFingerUp, OnFingerDown, and
OnFingerMove. There are two new methods you’ll be
adding: ApplicationEngine::PopulateVisuals and
MapToButton.
Example 8. ApplicationEngine implementation with tab bar
ApplicationEngine::ApplicationEngine(IRenderingEngine* renderingEngine) : m_spinning(false), m_renderingEngine(renderingEngine), m_pressedButton(-1) { m_buttonSurfaces[0] = 0; m_buttonSurfaces[1] = 1; m_buttonSurfaces[2] = 2; m_buttonSurfaces[3] = 4; m_buttonSurfaces[4] = 5; m_currentSurface = 3; }
void ApplicationEngine::Initialize(int width, int height) { m_trackballRadius = width / 3; m_buttonSize.y = height / 10; m_buttonSize.x = 4 * m_buttonSize.y / 3; m_screenSize = ivec2(width, height - m_buttonSize.y); m_centerPoint = m_screenSize / 2;
vector<ISurface*> surfaces(SurfaceCount); surfaces[0] = new Cone(3, 1); surfaces[1] = new Sphere(1.4f); surfaces[2] = new Torus(1.4f, 0.3f); surfaces[3] = new TrefoilKnot(1.8f); surfaces[4] = new KleinBottle(0.2f); surfaces[5] = new MobiusStrip(1); m_renderingEngine->Initialize(surfaces); for (int i = 0; i < SurfaceCount; i++) delete surfaces[i]; }
void ApplicationEngine::PopulateVisuals(Visual* visuals) const { for (int buttonIndex = 0; buttonIndex < ButtonCount; buttonIndex++) { int visualIndex = m_buttonSurfaces[buttonIndex]; visuals[visualIndex].Color = vec3(0.75f, 0.75f, 0.75f); if (m_pressedButton == buttonIndex) visuals[visualIndex].Color = vec3(1, 1, 1); visuals[visualIndex].ViewportSize = m_buttonSize; visuals[visualIndex].LowerLeft.x = buttonIndex * m_buttonSize.x; visuals[visualIndex].LowerLeft.y = 0; visuals[visualIndex].Orientation = Quaternion(); } visuals[m_currentSurface].Color = m_spinning ? vec3(1, 1, 1) : vec3(0, 1, 1); visuals[m_currentSurface].LowerLeft = ivec2(0, 48); visuals[m_currentSurface].ViewportSize = ivec2(320, 432); visuals[m_currentSurface].Orientation = m_orientation; }
void ApplicationEngine::Render() const { vector<Visual> visuals(SurfaceCount); PopulateVisuals(&visuals[0]); m_renderingEngine->Render(visuals); }
void ApplicationEngine::OnFingerUp(ivec2 location) { m_spinning = false; if (m_pressedButton != -1 && m_pressedButton == MapToButton(location)) swap(m_buttonSurfaces[m_pressedButton], m_currentSurface); m_pressedButton = -1; }
void ApplicationEngine::OnFingerDown(ivec2 location) { m_fingerStart = location; m_previousOrientation = m_orientation; m_pressedButton = MapToButton(location); if (m_pressedButton == -1) m_spinning = true; }
void ApplicationEngine::OnFingerMove(ivec2 oldLocation, ivec2 location) { if (m_spinning) { vec3 start = MapToSphere(m_fingerStart); vec3 end = MapToSphere(location); Quaternion delta = Quaternion::CreateFromVectors(start, end); m_orientation = delta.Rotated(m_previousOrientation); } if (m_pressedButton != -1 && m_pressedButton != MapToButton(location)) m_pressedButton = -1; }
int ApplicationEngine::MapToButton(ivec2 touchpoint) const { if (touchpoint.y < m_screenSize.y - m_buttonSize.y) return -1; int buttonIndex = touchpoint.x / m_buttonSize.x; if (buttonIndex >= ButtonCount) return -1; return buttonIndex; }
Go ahead and try it—at this point, the
wireframe viewer is starting to feel like a real
application! |
6. Animating the Transition
The button-swapping strategy is clever but
possibly jarring to users; after playing with the app for a while, the
user might start to notice that his tab bar is slowly being re-arranged.
To make the swap effect more obvious and to give the app more of a fun
Apple feel, let’s create a transition animation that actually shows the
button being swapped with the main viewport. Figure 6 depicts this animation.
Again, no changes to the two rendering
engines are required, because all the logic can be constrained to
ApplicationEngine. In addition to animating the
viewport, we’ll also animate the color (the tab bar wireframes are drab
gray) and the orientation (the tab bar wireframes are all in the “home”
position). We can reuse the existing Visual class for
this; we need two sets of Visual objects for the
start and end of the animation. While the animation is active, we’ll
tween the values between the starting and ending visuals. Let’s also
create an Animation structure to bundle the visuals
with a few other animation parameters, as shown in bold in Example 9.
Example 9. ApplicationEngine declaration with transition animation
struct Animation { bool Active; float Elapsed; float Duration; Visual StartingVisuals[SurfaceCount]; Visual EndingVisuals[SurfaceCount]; };
class ApplicationEngine : public IApplicationEngine { public: // ... private: // ... Animation m_animation; };
|
Example 10
shows the new implementation of ApplicationEngine.
Unchanged methods are omitted for brevity. Remember, animation is all
about interpolation! The Render
method leverages the Lerp and
Slerp methods from our vector class library to
achieve the animation in a
surprisingly straightforward manner.
Example 10. ApplicationEngine implementation with transition
animation
ApplicationEngine::ApplicationEngine(IRenderingEngine* renderingEngine) : m_spinning(false), m_renderingEngine(renderingEngine), m_pressedButton(-1) { m_animation.Active = false;
// Same as in Example 3-17 .... }
void ApplicationEngine::Render() const { vector<Visual> visuals(SurfaceCount); if (!m_animation.Active) { PopulateVisuals(&visuals[0]); } else { float t = m_animation.Elapsed / m_animation.Duration; for (int i = 0; i < SurfaceCount; i++) { const Visual& start = m_animation.StartingVisuals[i]; const Visual& end = m_animation.EndingVisuals[i]; Visual& tweened = visuals[i]; tweened.Color = start.Color.Lerp(t, end.Color); tweened.LowerLeft = start.LowerLeft.Lerp(t, end.LowerLeft); tweened.ViewportSize = start.ViewportSize.Lerp(t, end.ViewportSize); tweened.Orientation = start.Orientation.Slerp(t, end.Orientation); } } m_renderingEngine->Render(visuals); }
void ApplicationEngine::UpdateAnimation(float dt) { if (m_animation.Active) { m_animation.Elapsed += dt; if (m_animation.Elapsed > m_animation.Duration) m_animation.Active = false; } }
void ApplicationEngine::OnFingerUp(ivec2 location) { m_spinning = false; if (m_pressedButton != -1 && m_pressedButton == MapToButton(location) && !m_animation.Active) { m_animation.Active = true; m_animation.Elapsed = 0; m_animation.Duration = 0.25f; PopulateVisuals(&m_animation.StartingVisuals[0]); swap(m_buttonSurfaces[m_pressedButton], m_currentSurface); PopulateVisuals(&m_animation.EndingVisuals[0]); } m_pressedButton = -1; }
|
That completes the wireframe viewer! As you
can see, animation isn’t difficult, and it can give your application
that special Apple touch.